import { FSWatcher, watch } from 'chokidar'
import fs from 'fs-extra'
import path from 'path'
import { PluginContext } from 'rollup'
import { EventEmitter } from 'stream'
import { CWD } from './constances'
import { BaseResolvedGlobConfig } from './types'
import { toArray } from './utils'

export class GlobImportWatch extends EventEmitter {
  private _w!: FSWatcher
  private _files!: string[]
  private _code!: string
  constructor() {
    super()
  }

  // @ts-ignore
  on(event: 'codeGenerate', listener: (code: string, files: string[]) => void): this
  // @ts-ignore
  off(event: 'codeGenerate', listener: (code: string, files: string[]) => void): this

  // @ts-ignore
  on(event: 'add', listener: (fileName: string, stat: fs.Stats) => void): this
  // @ts-ignore
  off(event: 'add', listener: (fileName: string, stat: fs.Stats) => void): this

  // @ts-ignore
  on(event: 'change', listener: (fileName: string, stat: fs.Stats) => void): this
  // @ts-ignore
  off(event: 'change', listener: (fileName: string, stat: fs.Stats) => void): this

  // @ts-ignore
  on(event: 'unlink', listener: (fileName: string) => void): this
  // @ts-ignore
  off(event: 'unlink', listener: (fileName: string) => void): this

  get files() {
    return Array.from(this._files)
  }

  get code() {
    return this._code
  }

  init(context: PluginContext, config: BaseResolvedGlobConfig, output: (code: string) => any) {
    const { glob, exclude, load, invalidateOnChanged } = config

    const watcher = (this._w = watch(glob, {
      ignored: exclude,
      ignoreInitial: false,
      alwaysStat: true,
      ignorePermissionErrors: true,
      atomic: true,
      cwd: CWD,
    }))

    return new Promise<void>((resolve, reject) => {
      const files = new Set<string>()
      let ready = false

      const emitGenerateCode = async () => {
        const _files = (this._files = Array.from(files))

        const _code = (this._code = toArray(await load.call(context, _files, config)).join('\n'))
        await output(_code)
        this.emit('codeGenerate', _code, _files)
      }

      watcher.on('add', (file, stat) => {
        file = path.normalize(file)
        files.add(file)
        this.emit('add', file, stat)
        if (ready) {
          emitGenerateCode()
        }
      })

      watcher.on('change', (file, stat) => {
        file = path.normalize(file)
        this.emit('change', file, stat)
        if (ready && invalidateOnChanged) {
          emitGenerateCode()
        }
      })

      watcher.on('unlink', (file) => {
        file = path.normalize(file)
        files.delete(file)
        this.emit('unlink', file)
        if (ready) {
          emitGenerateCode()
        }
      })

      watcher.on('ready', () => {
        ready = true
        Promise.resolve(emitGenerateCode()).then(resolve)
      })

      watcher.on('error', (err) => {
        reject(err)
      })
    })
  }

  close() {
    this._w?.close()
  }
}

export const globImportWatch = async (
  context: PluginContext,
  config: BaseResolvedGlobConfig,
  output: (code: string) => any
) => {
  const watcher = new GlobImportWatch()
  await watcher.init(context, config, output)
  return watcher
}
